//@supresswarnings
import java.util.ArrayList;
import java.util.PriorityQueue;

/**
 * Tabla que almacenará todos los posibles simbolos del programa.
 * @author paco
 */
public class TablaSimbolos
{

    public static final int NOEXISTE = 0,
                            ESPARAMETRO = 1,
                            ESVARIABLE = 2;

    /**
     * Numero que ocupa el simbolo en la tabla
     */
    private int numSimbolo;
    /**
     * Tabla que contiene la tabla actual, en caso de no ser del ambito principal
     */
    private TablaSimbolos padre;
    /**
     * Simbolos que se almacenan en la tabla pertenecientes al ámbito actual
     */
    private ArrayList<Simbolo> simbolos;
    /**
     * Parametros que contiene el ambito
     */
    private ArrayList<Simbolo> parametros;
    /**
     * Conjunto de todas las variable de todos los ambitos que se han recorrido hasta el momento
     */
    private static PriorityQueue<Simbolo> variables;
    /**
     * Nivel de profundidad de la tabla
     */
    private int nivel;
    /**
     * Tabla de tipos asociada a la tabla de simbolos actual
     */
    private TablaTipos tablaTipos;

    /**
     * Constructor de la clase TablaSimbolos
     */
    public TablaSimbolos()
    {
        this.padre = null;
        this.simbolos = new ArrayList<Simbolo>();
        this.nivel = 0;
        if (variables == null)
        {
            this.variables = new PriorityQueue<Simbolo>();
        }
        parametros = new ArrayList<Simbolo>();
        numSimbolo = 0;
    }

    /**
     * Constructor de la clase TablaSimbolos
     * @param padre Tabla de simbolos padre
     * @param simbolos Simbolos a añadir
     * @param nivel Nivel de la tabla
     */
    public TablaSimbolos(TablaSimbolos padre, ArrayList<Simbolo> simbolos, int nivel)
    {
        this.padre = padre;
        this.simbolos = new ArrayList<Simbolo>();
        this.simbolos = (ArrayList<Simbolo>) simbolos.clone();
        this.nivel = nivel;
        parametros = new ArrayList<Simbolo>();
    }

    /**
     * Constructor
     * @param tt Tabla de tipos
     */
    public TablaSimbolos(TablaTipos tt)
    {
        tablaTipos = tt;
        parametros = new ArrayList<Simbolo>();
    }

    /**
     * Constructor de copia
     * @param t Tabla simbolos a copiar
     */
    public TablaSimbolos(TablaSimbolos t)
    {
        padre = t.padre;
        simbolos = t.simbolos;
        tablaTipos = t.tablaTipos;
        parametros = t.parametros;
    }

    /**
     * Retira un nivel de la tabla de simbolos, y devuelve las variables locales.
     * @return Cadena con los datos que se encuentran en la tabla del nivel actual.
     */
    public String toString(String h)
    {
        String ret = h + ".locals (";
        boolean first = true;
        boolean escribir = true;
        if (simbolos != null)
        {
            if (variables == null)
            {
                variables = new PriorityQueue<Simbolo>();
            }
            variables.addAll(simbolos);
        }

        while (variables != null && !variables.isEmpty())
        {
            Simbolo s = variables.poll();
            if (!first && escribir)
            {
                ret += ", ";
            }
            first = false;
            if (s.tipo == Tipo.INT || s.tipo == Tipo.BOOLEAN)
            {
                ret += "int32";
                escribir = true;
            } else if (s.tipo == Tipo.DOUBLE)
            {
                ret += "float64";
                escribir = true;
            } else if (tablaTipos.isArray(s.tipo))
            {
                Integer tipo = tablaTipos.getTipoBase(s.tipo);
                if (tipo == Tipo.INT || tipo == Tipo.BOOLEAN)
                {
                    ret += "int32[]";
                } else if (tipo == Tipo.DOUBLE)
                {
                    ret += "float64[]";
                }
                escribir = true;
            } else if ( s.isClass() ) 
            {
                ret += "class '" + tablaTipos.get(s.getTipo()).getNombre() + "'";
            }
            else
            {
                escribir = false;
            }
        }

        ret += " )\n";
        return ret;
    }

    public ArrayList<Simbolo> getSimbolos()
    {
        return simbolos;
    }
    
    /**
     * Borra el nivel actual de la tabla de simbolos
     */
    public void pop()
    {
        if (variables == null)
        {
            variables = new PriorityQueue<Simbolo>();
        }

        for (Simbolo s : simbolos)
        {
            variables.offer(s);
        }

        if (padre != null)
        {
            simbolos = padre.simbolos;
            padre = padre.padre;
            nivel--;
        }
    }

    /**
     * Elimina un nivel, en el caso especial de ser un metodo
     * @param clase Clase actual
     */
    public void popMetodo( Tipo clase )
    {
        if ( clase != null && clase.getAtributos() != null )
        {
            int indice = clase.getAtributos().getSimbolos().size();
            Simbolo s = clase.getAtributos().getSimbolos().get(indice - 1);
            if ( s.isMetodo() )
            {
                String locals = this.toString("");
                s.setLocals( locals );
            }
        }
        if (padre != null)
        {
            simbolos = padre.simbolos;
            padre = padre.padre;
            nivel--;
        }
    }
    
    /**
     * Añade un nivel mas a la tabla de simbolos
     */
    public void push()
    {
        padre = new TablaSimbolos(this);
        simbolos = new ArrayList<Simbolo>();
        nivel++;
    }

    /**
     * Añadir un simbolo a la tabla.
     * @param nombre Nombre del simbolo a añadir.
     * @param tipoSimbolo Tipo del simbolo que se va a añadir.
     * @param tipo Tipo del simbolo.
     */
    public int nuevoSimbolo(String nombre, Integer tipoSimbolo, Integer tipo)
    {
        if ( simbolos == null )
            simbolos = new ArrayList<Simbolo>();
                   
        Simbolo s = new Simbolo(nombre, tipoSimbolo, tipo, numSimbolo);
        numSimbolo++;
        
        if ( tablaTipos.get(tipo).isClass() )
            s.setClase();
        simbolos.add(s);
        return (numSimbolo - 1);
    }
    
    public int nuevoMetodo( String nombre, int tipoDev, int numParametros)
    {
        if (simbolos == null)
        {
            simbolos = new ArrayList<Simbolo>();
        }

        Simbolo s = new Simbolo(nombre, tipoDev, true);
        s.setMetodo();
        s.setNumParametros(numParametros);
        
        if ( tipoDev == -1 && numParametros == 0)
        {
            s.setConstructor();
        }

        return this.nuevoSimbolo(s);
    }
    
    public int nuevoSimbolo ( Simbolo s )
    {
        if ( simbolos == null )
            simbolos = new ArrayList<Simbolo>();
        s.setId(numSimbolo);
        numSimbolo++;
        simbolos.add(s);
        return (numSimbolo - 1);
    }

    /**
     * Añade un nuevo simbolo del tipo indice
     * @param name Nombre del indice
     * @return Posición que ocupa en la tabla
     */
    public int crearIndice(String name)
    {
        Simbolo s = new Simbolo(name, Simbolo.INDICE, Tipo.INT, numSimbolo);
        numSimbolo++;
        simbolos.add(s);
        return (numSimbolo - 1);
    }

    /**
     * Añade un nuevo simbolo del tipo Indice a la tabla
     * @param name Nombre del indice
     * @param tipo Tipo del indice (int, double, ... )
     * @return Posición que ocupa en la tabla
     */
    public int crearIndice(String name, int tipo)
    {
        Simbolo s = new Simbolo(name, Simbolo.INDICE, tipo, numSimbolo);
        numSimbolo++;
        simbolos.add(s);
        return (numSimbolo - 1);
    }

    /**
     * Devolver un simbolo dado. Si no existe, se busca en el padre. Para buscar usos
     * @param nombre
     * @return Devuelve un simbolo si se ha encontrado o null en caso contrario.
     */
    public Simbolo busca(String nombre)
    {
        if (simbolos != null)
        {
            for (Simbolo s : simbolos)
            {
                if (s.nombre.equals(nombre) )
                {
                    //Si es de la misma clase o publico
                    return s;
                    //Si no return null
                }
            }
        }
        if ( parametros != null )
        {
            for (Simbolo s : parametros)
            {
                if (s.nombre.equals(nombre))
                {
                    //Si es de la misma clase o publico
                    return s;
                    //Si no return null
                }
            }
        }
        // si no lo encuentra, busca en el ámbito exterior
        if (padre != null)
        {
            return padre.busca2(nombre);
        }

        return null; // no se encontró
    }
    
   public Simbolo busca2(String nombre)
    {
        if (simbolos != null)
        {
            for (Simbolo s : simbolos)
            {
                if (s.nombre.equals(nombre) && ( s.isPublic() || !s.isCampo() ) )
                {
                    //Si es de la misma clase o publico
                    return s;
                    //Si no return null
                }
            }
        }
        if ( parametros != null )
        {
            for (Simbolo s : parametros)
            {
                if (s.nombre.equals(nombre))
                {
                    //Si es de la misma clase o publico
                    return s;
                    //Si no return null
                }
            }
        }
        // si no lo encuentra, busca en el ámbito exterior
        if (padre != null)
        {
            return padre.busca2(nombre);
        }

        return null; // no se encontró
    }
  

    /**
     * Busca un simbolo en la tabla actual o en sus ambitos superiores si no lo encuentra
     * @param id Identificador del simbolo que se busca
     * @return Simbolo que corresponde al identificador indicado
     */
    public Simbolo busca(Integer id)
    {
        if ( simbolos != null )
        {
            for (Simbolo s : simbolos)
            {
                if (s.id == id)
                {
                    return s;
                }
            }
        }
        // si no lo encuentra, busca en el ámbito exterior
        if (padre != null)
        {
            return padre.busca(id);
        }

        return null; // no se encontró
    }

    /**
     * Busca un metodo con un número de argumentos
     * @param metodo Nombre del método que se busca
     * @param parametros Numero de parámetros que tiene el metodo
     * @return Retorna el metodo si lo encuentra y null en caso contrario.
     */
    public Simbolo buscar( String metodo, int parametros )
    {
        if ( simbolos != null )
        {
            for (Simbolo s : simbolos)
            {
                if ( s.getNombre().equals(metodo) && s.getNumParametros() == parametros )
                {
                    return s;
                }
            }
        }
        // si no lo encuentra, busca en el ámbito exterior
        if (padre != null)
        {
            return padre.buscar(metodo,parametros);
        }

        return null; // no se encontró

    }
    
    /**
     * Añade un simbolo con un número de parametros indicado
     * @param metodo Nombre del metodo
     * @param parametros Numero de parametros
     */
    public void addSimbolo( String metodo, int parametros )
    {
        Simbolo s = new Simbolo(metodo, numSimbolo, true);
        numSimbolo++;
        s.setNumParametros(parametros);
        s.setMetodo();
        simbolos.add(s);
    }

    /**
     * Comprueba si existe una variable en un ambito.
     * @param nombre Nombre del simbolo a buscar
     * @return Devuelve cierto si se encuentra y falso en caso contrario.s
     */
    public boolean existe(String nombre)
    {
        //devolver un entero que devuelva el tipo del simbolo (no existe, loc, arg o fld ) ("", "loc", "arg", "fld" )
        for (Simbolo s : simbolos)
        {
            if (s.nombre.equals(nombre))
            {
                return true;
            }
        }
        return false;
    }

    /**
     * Busca un simbolo en las tablas de simbolos y de parametros
     * @param nombre Nombre del simbolo que se busca
     * @return Devuelve en cual de las dos tablas se encuentra o si no se encuentra en ninguna de las dos.
     */
    public int localizar(String nombre)
    {
        for (Simbolo s : simbolos)
        {
            if (s.nombre.equals(nombre))
            {
                return ESVARIABLE;
            }
        }
        for (Simbolo s : parametros)
        {
            if (s.nombre.equals(nombre))
            {
                return ESPARAMETRO;
            }
        }

        return NOEXISTE;
    }

    /**
     * Searches for the first occurence of the given argument, testing for equality using the equals method.
     * @param nombre Nombre del Simbolo que se busca
     * @return the index of the first occurrence of the argument in this list; returns -1 if the object is not found.
     */
    public Integer getPos(String nombre)
    {
        if ( simbolos != null )
        {
            for (Simbolo s : simbolos)
            {
                if (s.nombre.equals(nombre))
                {
                    return s.id;
                }
            }
        }
        if (padre != null)
        {
            return padre.getPos(nombre);
        }

        return -1;
    }

    /**
     * Intenta añadir un parametro a la lista.
     * @param s Nombre del parametro que se va a insertar
     * @return Devuelve cierto si se ha podido insertar el parámetro, y falso en caso contrario
     */
    public boolean addParametro( String s )
    {
        if ( parametros == null )
            parametros = new ArrayList<Simbolo>();

        if ( localizar( s ) == NOEXISTE )
        {
            Simbolo sim = new Simbolo(s, Tipo.PARAMETRO, Tipo.DOUBLE, numSimbolo);
            numSimbolo++;
            parametros.add(sim);
            return true;
        }
        return false;
    }
    
    public Simbolo getMetodo( String nombre )
    {
        if ( simbolos != null )
        {
            for ( Simbolo s : simbolos )
            {
                if ( s.isMetodo() && s.getNombre().equals(nombre) )
                    return s;
            }
        }
        if (padre != null)
        {
            return padre.getMetodo(nombre);
        }
        return null;
    }
    
    public void clearParametros()
    {
        if ( parametros != null )
        this.parametros.clear();
    }
}
